feat(service-analytics)!: refuse an aggregate a datetime measure's field type cannot carry, and reconcile the storage-form annotations to one measured statement - #16778
Conversation
…cannot carry WIP — compile-leg refusal + the four reconciled datetime-storage annotations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…g-datetime-measure
…rce fields The full table refuses `min`/`max` over the string classes and the boolean rows, both of which this platform answers on purpose and pins with tests (#15768, maintainer ruling #11152). Executing those is a product judgement that belongs to #16099; the temporal rows carry no such collision and are the ones this card is about. Re-points the two `measure-result-type.test.ts` fixture measures that aggregated a datetime column, and corrects the module header that recorded the missing refusal as an open finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ec4a03b5619a2a8415adf00e3a62c7e5806f677d && git checkout ec4a03b5619a2a8415adf00e3a62c7e5806f677d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c930f859714de408ba0221f435ff957ed9e64759 181d3cc8a7727c3c80f7cef470ecf0339b46924b && git checkout -B drift-repro c930f859714de408ba0221f435ff957ed9e64759 && git merge --no-ff 181d3cc8a7727c3c80f7cef470ecf0339b46924b
node scripts/docs-audit/affected-docs.mjs --json c930f859714de408ba0221f435ff957ed9e64759
|
Contract review (
|
Brings #16750 (boolean/toggle rows added to sum/avg/min/max in the aggregate x field-type compatibility table), which settles the boolean half of the collision this branch reported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…class it actually withdraws The compile leg was scoped to TEMPORAL source fields in 80ec9f2, but the changeset and the ADR-0087 ledger entry still described the pre-scoping full-table gate. A breaking-change record that overstates what changed tells every consumer reading the release notes that behaviour was withdrawn which was not. F1 — the breaking declaration: - changeset FROM/TO: the two `percent` rows dropped (`sum` x `percent` is a table row this leg does not execute; `avg` x `percent` was never a migration at all), and the surviving `avg` row widened to name all three temporal members it does refuse. - changeset: a new scope section states the temporal scope outright, and that the string rows sit under #16785 (ruled C - the table is to be AMENDED to accept them), the boolean rows were settled as ACCEPT by #16685 / #16750, and `sum` x `percent` is not executed here. - ledger `surface`: scoped to the temporal class; "sum over a percent" removed. - ledger `replacement`: the "`avg` for a `percent`" prescription dropped with the surface rows it belonged to. - ledger `acceptanceCriteria`: qualified to a measure over a `date` / `datetime` / `time` field, and says outright that a field of any other class is neither refused nor certified by this leg. - `registry.ts` REGENERATED with `pnpm --filter @objectstack/spec gen:migration-registry`, never hand-edited; two consecutive runs are byte-identical and the diff is confined to this entry's block. - also corrected: the changeset said "four contradictory annotations"; the sweep reconciled seven source sites plus two test narratives. F2 — the boolean collision is settled, so stop narrating it as live. #16685 was ruled A and #16750 added `boolean` / `toggle` to the four arithmetic / order rows, so the table ACCEPTS them. Reworded in `dataset-compiler.ts`'s scope docblock, the refusal suite's module header, and `measure-result-type.ts` (whose boolean paragraph still referred a missing refusal onward). All three now also record #16785 C for the string rows. F3 — the ledger `reason` presented both dialect halves as measured alike. The SQLite half is pinned by a live `sql.js` suite; the Postgres 42883 half was measured in-session and is pinned by nothing. Said so where it is stated. F4 — the scope-boundary test asserted `isAggregateCompatibleWithFieldType( 'min', 'text') === false`, a verdict #16785 C is about to amend. Dropped: the case now pins only what this PR owns - a non-temporal field is not judged, so the measure compiles and SQL is emitted. Refutability is carried by a second case on `sum` x `text`, a row no ruling is moving, plus a non-vacuity assertion that SQL reached the driver in both. F5 — the changeset now names the two uncovered faces: `/analytics/query` and any `compileDataset` caller wiring no `declaredFieldType` probe. Refs #16737. Review: PR #16778 contract review, comment 5580295870. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Contract review (
|
| finding | status | evidence |
|---|---|---|
| F1 (blocking) — declaration overstated the narrowing | discharged | changeset :61-79, :87-89; ledger surface/acceptanceCriteria scoped to date/datetime/time; registry.ts block = entry text, sorted position, 192 = 192; ADR-0087 gate exit 0 |
| F2 — stale boolean narrative ×3 | discharged | dataset-compiler.ts scope docblock, test header :44-59, measure-result-type.ts:131-138 all say #16685 A / #16750 ACCEPT and #16785 C; git merge-base --is-ancestor ed7243d52 refs/review/16778 → yes (via merge 31888c210) |
| F3 (optional) — Postgres half asserted | discharged | ledger reason: "SQLite half is PINNED by a live sql.js suite … Postgres half was MEASURED IN-SESSION … not pinned by any test"; sql.js is a devDependency (package.json:36) |
| F4 — boundary test pinned the #16785 C row | discharged | min×text table assertion gone (:485-503); sum×text + sqls.length === 1 non-vacuity (:505-518) |
| F5 (optional) — uncovered faces not in changeset | discharged | changeset :102-107 names /analytics/query and any probe-less compileDataset caller |
| F6 — CI tree predated #16750 | discharged | #16750 in ancestry; all CI on the merged head green except N1 |
New findings
- (record)
Part-of PR must not also close its cardis red on the head for a reason the first review could not see: commit181d3cc8a's message endsRefs #16737. Review: PR #16778 contract review, comment 5580295870.origin/main's guard (scripts/check-partof-closing-keyword.mjs, the commit-list half readingPR_COMMITS_FILE) refuses any card trailer on a commit — the.claude/agents/os-dev.mdrule "commit ⛔ 不带卡片 trailer". Not a body contradiction (item 5). The gate's own text: advisory at branch protection (absent fromREQUIRED_CONTEXTS, nomerge_grouptrigger), nothing an author can do on a pushed branch, and what discharges it is a squash whose message is the PR body. I confirmed the gate's premise onmain:0a61db1f5(the squash of fix(tooling): isolate git children from ambient GIT_*, and make a shared core.bare flip loud #16646) carriesRefs #16624verbatim, i.e. the repo's squash message is assembled from commit messages, not the body. A queue merge of this PR therefore landsRefs #16737inmain's history — a reference only, it moves no card — unless the lander replaces the message by hand. - (observation) Tiering is fail-OPEN on every stand-down (item 1). That is the spec's ruled posture and is stated in the ledger
acceptanceCriteria, but note the practical consequence for the two acknowledged faces:/analytics/queryand any host that wires nosourceFieldMetakeep producing the average-year number silently. No layer refuses an incoherent aggregate / field-type pair — a dataset measureavgover a datetime works on SQLite and errors on Postgres #16099 owns closing them; nothing here regresses. - (observation)
check-closing-keyword-parityNOT MEASURED locally (dependency absent); relied on CI green.check:migration-registrynot executed locally either (TS generator, needs the built closure) — the registry block was verified by reading the entry and the generated block side by side plus the count and sort position, and the PR body records exit 0 with an idempotence hash; CILint & Repo Gatesgreen. - (observation)
avg×Field.timeis a genuine dialect split (Postgres answers a clock time). The table refuses it and the gate executes that; already recorded in the body's 验收备注 5 — flagged only so a maintainer sees it before merge, no action asked.
Maintainer-only merge: yes. Not for a governed path (none touched) but for three things only a maintainer seat can do at once: the PR is a draft carrying needs:contract-review; it is a ! BREAKING accept-set narrowing under an ADR-0087 marker (a disposition, not a fix); and N1 means the merge button, not the queue, is where the Refs #16737 residue is either dropped (replace the squash message with the PR body) or accepted into history — a queue merge edits nothing.
Not done by this seat
No approval, no request-changes, no label, no edit, no merge action. Throwaway ref refs/review/16778 deleted; git show-ref confirms it is gone.
Generated by Claude Code
Fixes #16737
Also implements the compile leg described on #16099 — that card stays open and its
disposition is the PM's; nothing here is a verdict about it.
1. The storage reality, established first — and it is a THIRD answer
The card said the package states two incompatible answers. Measured on
origin/main,neither is current. A SQLite
Field.datetimecolumn has ONE storage form:canonical UTC TEXT,
YYYY-MM-DDTHH:MM:SS.sssZ(#3912/#3928).How it was measured, not recalled:
SqlDriver.storageDatetimeValuecanonicalises on the write path, andsql-driver-datetime-canonical-storage.test.tspins every accepted input shapefolding onto the same stored string — a
Date, ISO…Z, ISO with an offset, a naivewall clock, an epoch number, an epoch string, a bare calendar day. The
NOW()columndefault writes the same
strftime('%Y-%m-%dT%H:%M:%fZ', 'now')bytes.initObjectsrunsbackfillCanonicalDatetimesat schema sync;needsLegacyDatetimeRepairis the one predicate for "might this column still hold apre-canonical value". Two cases keep it true: a table not yet backfilled, and an
external / unmanaged object (
registerExternalObjectnever marks its datetime columnscanonical).
timestamptz/DATETIME(3), one on-disk shape by construction.Re-driven live, in this session, on a fresh SQLite:
So the filer's
text|2026-05-19T00:00:00.000Zreading is exactly what the currentdriver produces. ⭐ The mixed INTEGER/TEXT column is the transitional state, and the
flat "a SQLite
Field.datetimeIS an INTEGER epoch" has been wrong since #3912.2.
coerceTemporal— the thing triage named as unmeasuredIt is correct for the mixed-storage reality, and it is correct for the canonical one.
Its docblocks were the stale part.
NativeSQLStrategy.coerceTemporal→ctx.coerceTemporalFilterValue→
SqlDriver.temporalFilterValue→coerceFilterValue) canonicalises to the samefunction the write path uses, so an ISO or bare-day comparand becomes the stored form.
Pinned in
sql-driver-temporal-dialect.test.tsandsql-driver-analytics-datetime.test.ts.temporalColumn→temporalFilterColumnSql) emits the repair CASEonly while
needsLegacyDatetimeRepairholds, and the bare column otherwise —pinned by
sql-driver-datetime-canonical-storage.test.ts(
expect(driver.temporalFilterColumnSql('evt','at','"at"')).toBe('"at"')after backfill).No code change was needed there. A test case was added on the analytics side
(
native-sql-datetime-filter.test.ts) driving the strategy against a hook that returnstoday's canonical UTC text, beside the epoch hook the file was written with — so the
suite covers the live storage form as well as the legacy one.
3. The annotations reconciled — seven sites, not the one the filer quoted
The card named four. A sweep for the claim found seven in source plus two test
narratives, all making the same stale statement. All are now consistent, and the fact is
stated once:
analytics-service.tscoerceTemporalFilterValueField.datetime→ epoch ms"analytics-service.tscoerceTemporalFilterColumn(:493)plugin.ts(:640)plugin.ts(:662)native-sql-strategy.tstemporalColumn(:963)native-sql-strategy.tsbuildFilterClause(:1190)objectql-strategy.tsdateRangeBounds(:1669)Field.datetimeIS an INTEGER epoch (#2034)"NativeSQLStrategyneeds the coercion__tests__/native-sql-datetime-filter.test.ts__tests__/native-sql-datetime-filter-column.test.tsEPOCH_MS(...)re-labelled as a marker, not a claim about emitted SQL4. Refusal, not a definition — and where it stops
Refusal. A definition would have to pick one of two dialect answers and ship it as
the meaning of a number nobody can check. The refusal moves the failure to the person
writing the dashboard, which is the PM direction and the card's own danger analysis.
⛔ It is not a new rule. The contract already exists:
AGGREGATE_FIELD_TYPE_COMPATIBILITYin
@objectstack/spec(#16353, landed as6ba0db4e0), declared under the directorruling of decision batch #59. Until this PR it had zero consumers.
compileDatasetnow reads the shipped predicate and refuses with
DATASET_INVALID/ 400 before anyquery is built, using the declared type the host already supplies through
AnalyticsServiceConfig.sourceFieldMeta(threaded as a new optionalDatasetCompileOptions.declaredFieldTypeprobe, tiered "cannot answer, do not block").The dispatch said: stop and report if refusing would break a use that currently works
and is legitimate. Executing every row of the table did exactly that when this branch
was cut. Where the two families stand now:
min/maxover the STRING classes (text,select,lookup,autonumber, …)— the table refuses them; this gate does not enforce that.
measureResultType(A dataset measure over a datetime is typed
numberin the analytics response, and a metric tile ignores theformatstyle it is handed — measured on 17.3.0 #15768) types those results as'string', andmeasure-result-type.test.tspins themend to end through
queryDataset. Measured: enforcing those rows turns 15 cases red.⇒ [Decision] The ruled aggregate × field-type table cannot be executed in full: its string rows contradict #15768's typing and its boolean rows contradict ruling #11152 — enforce, amend, or leave partly decorative? #16785 has since been ruled C: the TABLE is to be amended to accept
min/maxover the string classes. Enforcing them from here would pre-empt a ruling that goes the
other way.
boolean/toggleare settled, and are no longer a collision. [Decision] Two maintainer rulings collide on boolean aggregates — batch #59's "every other pair refused" would refuseavg(flag), which ruling #11152 pins on six backends as having no per-aggregate exception #16685 was ruled Aand PR feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (#16685) #16750 added both members to the
sum/avg/min/maxrows, on theauthority of maintainer ruling [finding]
AGGREGATION_ROWShas no boolean column, so the cross-driver aggregation conformance family cannot see a boolean aggregand on any face #11152 (booleans aggregate as numbers on every backend,AGGREGATION_CASES). The table now accepts them, so nothing refuses them anywhere— and this gate never judged them either way, because they are outside the temporal
class.
mainis merged into this branch (merge commit31888c210) so the tree this PRis measured on contains that decision.
⇒ The gate judges the temporal class only (
TEMPORAL_SOURCE_FIELD_TYPES, thispackage's own shipped statement of it). The verdict is still the table's — nothing is
restated, and
min/maxover a temporal field stay accepted because the table acceptsthem. The temporal rows carry no counter-evidence: measured on both dialects, no shipped
dataset in this repo pairs them, and there is no reading on which the mean of a set of
instants is a duration. The full-table leg stays with #16099, now waiting on #16785's
amendment landing rather than on two unruled collisions. The table's
sum×percentrow is likewise not executed by this leg.
5.
derivedis covered by constructionA derived measure's
ofoperands are base measures of the same dataset, and the executorexpands a selected derived measure into them before querying. Both are downstream of the
compile loop, so a dataset carrying a refused base measure never finishes compiling and no
derivedop can be handed its output — including when the selection names only thederived measure, which is the filer's exact shape. Pinned as two cases, one of them
selecting only
cycle_delta.6. Second dialect — measured on real PostgreSQL 16.13
Which half is dialect-specific: the SILENT half is SQLite's. SQLite has no temporal
type, so the stored text is coerced to a number by its leading digits and the call
succeeds. Postgres has a real
timestamptzand noavgover it, so it refuses loudly.The MEANINGLESS half is not dialect-specific — there is no backend on which the mean
of a set of instants is a duration; Postgres simply says so out loud.
select avg('12:00:00'::time)succeeds on Postgresand returns a clock time, while SQLite silently answers
13(the average hour) for thesame data. So
avgover aField.timeis a genuine per-dialect split. The ruled tablerefuses it (
timeappears only in themin/maxrows), and that ruling is executed hererather than re-litigated.
SQLite half is PINNED by a live
sql.jssuite in the new test file. The Postgres42883half was measured in-session on a real PostgreSQL 16.13 started in this container and is
pinned by no test — the live PG conformance job carries no cell for it. Nothing
depends on it: the refusal is decided from declared metadata before a driver is reached.
7. Negative controls — this card is aggregation only
All pinned in
aggregate-datetime-measure-refusal.test.ts:avgover anumbermeasure, and over acurrencymeasure — still work end to end,one statement emitted,
AVGin the SQL.datetimeused as a dimension — month grouping, and adateRangewindow — bothreach the engine unchanged (the window asserted present in the engine call).
min/maxover adatetime— still accepted, still return real instants.countover adatetime— still accepted.derivedmeasure over numeric operands — untouched.cases:
minover atextfield still compiles here whatever the table says aboutthat pair — the direct
isAggregateCompatibleWithFieldType('min','text') === falseassertion is gone, because [Decision] The ruled aggregate × field-type table cannot be executed in full: its string rows contradict #15768's typing and its boolean rows contradict ruling #11152 — enforce, amend, or leave partly decorative? #16785 C amends exactly that row and a test of this card
must not be what stands in the way of it. Refutability is carried by a second case on
sumovertext, a row no ruling is moving, plus a non-vacuity assertion in both thatSQL actually reached the driver — a widened gate refuses before any SQL, so
sqlswould be empty and both cases go red.
Tests
The dependency closure was built before any of the above
(
pnpm --filter '@objectstack/service-analytics^...' build), and the rebuilt@objectstack/specdistwas read back to prove the merged table is what the tests see:AGGREGATE_FIELD_TYPE_COMPATIBILITY.avg=number,currency,percent,rating,slider,progress,summary,boolean,toggle— #16750's rows present, so no verdict below was taken against a stale build.
Dissolution verification — direction predicted before running, mutation proved on disk
Deleting the
assertAggregateFieldTypeCompatiblecall from the measure loop, re-run onthe patched head:
The 7 red are exactly the refusal cases (both
derivedcases among them) plus thescope-boundary suite's "every temporal member IS judged" — so the boundary suite made
neutral under F4 is still refutable, which is the point of the F4 change. The 17 green are
the negative controls, the tiering tiers, the two scope-boundary compile cases, the
contract-table assertions and the live SQLite measurement suite. No case can pass
vacuously: each refusal case asserts the envelope AND that no SQL was emitted, and with
the gate gone the compile succeeds and SQL IS emitted. Restored under a
trap … EXIT INT TERMwithgit checkout HEAD -- ABSPATH(not a bare checkout), provedby blob equality plus an empty
git diff HEAD, never by an exit code.⛔ No rebuild leg was needed for the ablation and none is claimed: the mutated module and
the suite resolve through the same package's
src(the test imports../analytics-service.js,and
service-analyticshas no vitest alias). The ablation's own red is that proof — adist-mediated subject would have stayed green without a rebuild. The cross-package half
that IS dist-mediated (
@objectstack/spec, aKNOWN_UNALIASED_TEST_IMPORTSpair) wasrebuilt and property-read first, above.
Gates
node scripts/pm/dispatch-gates.mjs --commandsderived the family on the merged tree(commit
181d3cc8a); every command was run and reconciled:pnpm --filter @objectstack/lint check:doc-formula-expressions— first run exit 3(
@objectstack/lintnot built). Prerequisite satisfied (pnpm --filter '@objectstack/lint...' build) and re-run: exit 0.pnpm check:dual-build-cjs-loads— exit 3, PREREQUISITE NOT MET (needs a whole-farmpnpm build; 70+ packages withoutdist). NOT MEASURED, declared to CI.pnpm check:type-check-debt— exit 3, PREREQUISITE NOT MET (--re-measureneeds thebuilt closure of 23 workspace deps). NOT MEASURED, declared to CI. Its first half,
check:type-check-coverage, passed.Two families the derivation does not print, run explicitly:
pnpm check:route-envelope— exit 0. It is in dispatch-gates' silent"weakest verdict" bucket (dispatch-gates: a whole-tree-walk gate whose workflow
names:lists only its CURRENT members is placed Silent, so it is never derived for the card that adds a new member — measured on check:route-envelope / PR #16730 #16828). No module in this diff writes a response body; thethree regex hits across the diff are all inside comments or ledger prose.
pnpm --filter @objectstack/spec check:migration-registry— exit 0, and it isabsent from the derived union even though the diff regenerates
registry.ts. Worth alook as a second instance of the dispatch-gates: a whole-tree-walk gate whose workflow
names:lists only its CURRENT members is placed Silent, so it is never derived for the card that adds a new member — measured on check:route-envelope / PR #16730 #16828 shape.ADR-0087 and the ledger, at the patched head:
registry.tsis generated, never hand-edited: it was written bypnpm --filter @objectstack/spec gen:migration-registry, two consecutive runs produce abyte-identical file (
git hash-object=4c387065…both times,diffempty), and theresulting diff is confined to this entry's own block inside
step18. The working tree isclean after the whole gate sweep (
git status --porcelainempty), so no gate wrote abaseline behind the run.
Clause-② and the changeset
Clause-②: yes. The dispatch's provisional read wasno; the diff overturns it, onboth the mechanical floor and the gate:
The mechanical floor also applies on its own:⚠️
DatasetCompileOptions.declaredFieldTypeisa new key on a published exported type (
packages/services/service-analytics/src/index.tsre-exports
DatasetCompileOptions).needs:contract-reviewwas removed from thisPR when the review concluded; re-hanging it on the patched head is the PM seat's act, not
this branch's.
Changeset:
minorfor both packages, declared BREAKING with a!title. It is anaccept-set narrowing on a published authoring surface, which is breaking — and #16353's own
changeset says in writing that the consumer legs "carry the breaking declaration, its
migration prescription and the ADR-0087 disposition".
minorrather thanmajoris therepo's launch-window convention for accept-set narrowings, stated verbatim in the protocol
18 step's header ("The enforcement itself ships earlier on the 17.x line … this step is
where
migrate metausers are told"). ⛔ Notpatch: nothing here is "just a bug fix" —authoring shapes that parsed and ran now fail. The FROM → TO table is in the changeset body,
and it now describes only what this leg withdraws.
Patch round — what the contract review asked for, and what was done
percentrows dropped, and the survivingavgrow widened to name all three temporal members it does refuse. A new scope section states the temporal scope outright, names #16785 (ruled C) for the string rows, #16685/#16750 for the boolean rows, and sayssum×percentis not executed. Ledgersurface: scoped to the temporal class, "sum over a percent" removed. Ledgerreplacement: the "avgfor apercent" prescription dropped with the surface rows it belonged to. LedgeracceptanceCriteria: qualified to a measure over adate/datetime/timefield, and it now says outright that a field of any other class is neither refused nor certified here.registry.tsREGENERATED (idempotence + entry-block-only diff proved above).mainmerged (31888c210, bringsed7243d52= #16750;git merge-base --is-ancestor ed7243d52 HEADexit 0). Reworded indataset-compiler.ts's scope docblock, the refusal suite's module header, andmeasure-result-type.ts(whose boolean paragraph still referred a missing refusal onward — the table now ACCEPTS the pair, so none is owed). All three also record #16785 C for the string rows.reasonnow separates the two halves explicitly: SQLite PINNED by the livesql.jssuite, Postgres42883MEASURED IN-SESSION and pinned by nothing. Chose the "mark it" option over adding a PG conformance cell — the second would widen this card into the live-PG job, which the review left open as either/or./analytics/queryand anycompileDatasetcaller wiring nodeclaredFieldTypeprobe.One correction made beyond the review's list, in the same class as F1: the changeset said
"four contradictory annotations", which was the card's count. The sweep reconciled seven
source sites plus two test narratives (§3, and the review's own item 4 counts seven). The
changeset now says seven.
验收备注
Reported here for the PM, not filed as issues:
check:migration-registryis absent fromdispatch-gates --commandseven on adiff that regenerates
packages/spec/src/migrations/registry.ts. Same shape ascheck:route-envelope/dispatch-gates: a whole-tree-walk gate whose workflownames:lists only its CURRENT members is placed Silent, so it is never derived for the card that adds a new member — measured on check:route-envelope / PR #16730 #16828: a family that applies, derives as nothing, and is onlycaught because a human review named it. Both were run explicitly here.
avgover a datetime works on SQLite and errors on Postgres #16099 is still labelledpm:blockedwithBlocked-by: #16353, and spec: declare the aggregate × field-type compatibility matrix (AggregationFunction×FieldType) that dataset measures are refused against (spec half of #16099) #16353 haslanded (
6ba0db4e0, PR feat(spec): declare the aggregate × field-type compatibility matrix (AggregationFunction × FieldType) dataset measures are refused against (#16353) #16684). The blocker is stale; the label is the PM's to move.⛔ Untouched by this branch.
/analytics/query(the non-dataset face) is not covered by this leg. Anauto-inferred
submitted_at_avgmeasure on a Cube still reaches the driver — theruling names two legs (lint + compile) and neither is that face. Now also stated in the
changeset (F5). Observation, not a defect this PR introduces.
packages/lintstill carries no rulepairing a measure's aggregate with its field type. That is No layer refuses an incoherent aggregate / field-type pair — a dataset measure
avgover a datetime works on SQLite and errors on Postgres #16099's sibling devx card.avgover aField.timeis a real per-dialect split (Postgres answers a clocktime; SQLite answers the average hour). The table refuses it and this PR executes that;
worth a maintainer eye if anyone was relying on the Postgres behaviour.
avg(flag), which ruling #11152 pins on six backends as having no per-aggregate exception #16685/feat(spec): accept boolean / toggle for sum / avg / min / max in the aggregate × field-type table (#16685) #16750settled the boolean rows as ACCEPT; [Decision] The ruled aggregate × field-type table cannot be executed in full: its string rows contradict #15768's typing and its boolean rows contradict ruling #11152 — enforce, amend, or leave partly decorative? #16785 ruled C on the string rows but that
amendment has not landed in
@objectstack/specyet —isAggregateCompatibleWithFieldType('min','text')still answers
falseon this tree. No layer refuses an incoherent aggregate / field-type pair — a dataset measureavgover a datetime works on SQLite and errors on Postgres #16099's leg widens once it does.None of these were filed: a duplicate-search would be needed for each, and items 2 and 6
are dispositions on existing cards rather than new work.
🤖 Generated with Claude Code
https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Generated by Claude Code